<!DOCTYPE HTML>
<html>
<!--
Tor bug
https://trac.torproject.org/projects/tor/ticket/1517
-->
<head>
  <meta charset="utf-8">
  <title>Test for Tor Bug 1517 and Mozilla Bug 1424341</title>
  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
  <script type="application/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
</head>
<body>
<a target="_blank" href="https://trac.torproject.org/projects/tor/ticket/1517">Tor Bug 1517</a>
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1424341">Mozilla Bug 1424341</a>

<!-- Canvas for testing 'currentTime' -->
<canvas id="test-canvas" width="100" height="100"></canvas>

<!-- The main testing script -->
<script type="application/javascript">
  SimpleTest.requestFlakyTimeout("testing JS time-based fingerprinting");

  // Prepare for test of AudioContext.currentTime
  let audioContext = new AudioContext();
  // Prepare for test of CanvasStream.currentTime
  let canvas = document.getElementById("test-canvas");
  let context = canvas.getContext("2d");
  context.fillText("test", 20, 20);
  let canvasStream = canvas.captureStream(25);

  // Known ways to generate time stamps, in milliseconds
  const timeStampCodes = [
    "performance.now()",
    "new Date().getTime()",
    "new Event(\"\").timeStamp",
    "new File([], \"\").lastModified",
    "new File([], \"\").lastModifiedDate.getTime()",
  ];
  // These are measured in seconds, so we need to scale them up
  var timeStampCodesDOM = timeStampCodes.concat([
    "audioContext.currentTime * 1000",
    "canvasStream.currentTime * 1000",
  ]);

  let isRounded = (x) => {
    expectedPrecision = 2;
    let rounded = (Math.floor(x / expectedPrecision) * expectedPrecision);
    // First we do the perfectly normal check that should work just fine
    if (rounded === x || x === 0)
      return true;

  // When we're diving by non-whole numbers, we may not get perfect
  // multiplication/division because of floating points.
  // When dealing with ms since epoch, a double's precision is on the order
  // of 1/5 of a microsecond, so we use a value a little higher than that as
  // our epsilon.
  // To be clear, this error is introduced in our re-calculation of 'rounded'
  // above in JavaScript.
  if (Math.abs(rounded - x + expectedPrecision) < .0005) {
    return true;
  } else if (Math.abs(rounded - x) < .0005) {
    return true;
  }

    // Then we handle the case where you're sub-millisecond and the timer is not
    // We check that the timer is not sub-millisecond by assuming it is not if it
    // returns an even number of milliseconds
    if (expectedPrecision < 1 && Math.round(x) == x) {
      if (Math.round(rounded) == x) {
        return true;
      }
    }

    ok(false, "Looming Test Failure, Additional Debugging Info: Expected Precision: " + expectedPrecision + " Measured Value: " + x +
      " Rounded Vaue: " + rounded + " Fuzzy1: " + Math.abs(rounded - x + expectedPrecision) +
      " Fuzzy 2: " + Math.abs(rounded - x));

    return false;
  };

  // ================================================================================================
  // ================================================================================================

  function* checkWorker(worker) {
    // The child worker will send the results back.
    let checkWorkerTimeStamps = () => new Promise(function(resolve) {
      let onMessage = function(event) {
        worker.removeEventListener("message", onMessage);

        let timeStamps = event.data;
        for (let i = 0; i < timeStampCodes.length; i++) {
          let timeStamp = timeStamps[i];
          ok(isRounded(timeStamp),
             "'" + timeStampCodes[i] +
             "' should be rounded to nearest 2 ms in workers; saw " +
             timeStamp);
        }
        resolve();
      };
      worker.addEventListener("message", onMessage);
    });

    // Send the codes to its child worker.
    worker.postMessage(timeStampCodes);

    // First, check the child's results.
    yield checkWorkerTimeStamps();
    // Then, check the grandchild's results.
    yield checkWorkerTimeStamps();

    worker.terminate();
  }

  add_task(function*() {
    let worker = new Worker("worker_child.js");
    // Allow ~550 ms to elapse, so we can get non-zero
    // time values for all elements.
    yield new Promise(resolve => window.setTimeout(resolve, 550));
    yield checkWorker(worker);
  });

  // ================================================================================================
  // ================================================================================================


  add_task(function*() {
    // Loop through each timeStampCode, evaluate it,
    // and check if it is rounded
    for (let timeStampCode of timeStampCodesDOM) {
      let timeStamp = eval(timeStampCode);
      ok(isRounded(timeStamp),
         "'" + timeStampCode + "' should be rounded to nearest 2ms" +
         " saw " + timeStamp);
    }
  });

</script>


</body>
</html>
